/* 问题记录 */
// 1 appcan.window.open()
//   根据API的描述，其中的参数name定义窗口名，若该窗口存在直接打开，如果不存在先创建然后打开。
//   我的理解是若该窗口存在，open()后会将其置顶，但appcan模拟时是这样的，但在真机测试时发现并未置顶。
// 2 appcan.window.open()方法
//   连续调用此方法依次打开A窗口和B窗口，模拟时先打开了A窗口，然后在其上方又打开B窗口；真机测试时只能看到A窗口，即连续调用时只执行了第一次打开操作（貌似加了延迟会执行，猜测是第一次打开的动作过程中执行第二次的时候被忽略了）
// 3 appcan.window.close()方法关闭非顶层窗口后，该窗口的画面仍然存在，会遮挡其之前打开的非关闭的窗口 => 解决方法，关闭非顶层窗口前，先将其隐藏
(function(){
    // localStorage.wiAppWin_win {n, k} 缓存当前打开的所有窗口 name（由于 root 始终存在，故不记录）
    var ROOTWIN = 'root';
    var oOpenTimer = null;// 打开窗口时，开启定时器，用于限制重复打开
    /* 虽然在 frame 中可以通过 frame 及 opener 的判断实现监听 onWinFocus 和 onWinBlur，但为了保持 app 的 win 及 frame 的结构逻辑，且无法判断 frame 是否置顶，建议通过在 frame 的 opener 中 publish 来实现 */
    /**
     * @widoc $.onWinFocus
     * @namespace aux
     * @depends getUrlParams/$.getUrlParams, chanel/$.subscribeChanel
     * @des 定义窗口聚焦事件
     * @param {function} fCb() 当前窗口聚焦后执行的回调
     */
    $.onWinFocus = function(fCb){
        $.subscribeChanel({// 订阅：窗口聚焦
            channelId: 'wiAppWin_focus',
            callback: function(data){
                if(data.win === ($.getUrlParams().win || ROOTWIN)){
                    fCb && fCb();
                }
            }
        });
    };
    /**
     * @widoc $.onWinBlur
     * @namespace aux
     * @depends getUrlParams/$.getUrlParams, chanel/$.subscribeChanel
     * @des 定义窗口失焦事件
     * @param {function} fCb() 当前窗口聚焦后执行的回调
     */
    $.onWinBlur = function(fCb){
        $.subscribeChanel({// 订阅：窗口失焦
            channelId: 'wiAppWin_blur',
            callback: function(data){
                if(data.win === ($.getUrlParams().win || ROOTWIN)){
                    fCb && fCb();
                }
            }
        });
    };
    /**
     * @widoc $.openWin
     * @namespace aux
     * @depends getUrlParams/$.getUrlParams, chanel/$.publishChanel
     * @des 打开新窗口。
     * @param {object} opts
     * @param {string} opts.name 要打开的新窗口名，命名规则：opener模块名_opener文件名_*
     * @param {string} opts.url 不含参数的 url
     * @param {object=} opts.args 参数
     * 打开的新窗口中，除了用户传入的 args，还包括：
     * - opener：执行打开新窗口操作的源窗口名
     * - openerf：若存在，则打开新窗口的操作是在该 frame 中执行的
     * - win：新窗口的窗口名
     * 示例（此处 w1,w2,f1 不符合命名规则，仅为说明参数关系）：
     * - w1(opener:*, win:w1) => w2(opener:w1, win:w2)
     * - w1.f1(opener:w1, frame:f1) => w2(opener:w1, openerf:f1, win:w2)
     * @param {number=} opts.aniId 动画类型
     *  0: 无动画，1: 从左向右推入，2: 从右向左推入，3: 从上向下推入，
     *  4: 从下向上推入，5: 淡入淡出，6: 左翻页，7: 右翻页，8: 水波纹，
     *  9: 由左向右切入，10: 由右向左切入，11: 由上向下切入，12: 由下向上切入，
     *  13: 由左向右切出，14: 由右向左切出，15: 由上向下切出，16: 由下向上切出
     * @param {number=} opts.type 窗口类型
     *  0: 普通窗口，1: OAuth 窗口，2: 加密页面窗口，4: 强制刷新，
     *  16: view不透明，32: 隐藏的window，64: 等待popOver加载完毕后显示，128: 支持手势，
     *  256: 标记open的window上一个window不隐藏，512: 标记open的浮动窗口用友打开wabapp）
     */
    $.openWin = function(opts){
        if(oOpenTimer){
            return;
        }
        oOpenTimer = setTimeout(function(){
            oOpenTimer = null;
        }, 500);
        opts = $.extend({
            //name: '',
            //url: '',
            args: {},
            aniId: 10,// {number=} 动画类型
            type: 256// {number=} 窗口类型
            /* 以下参数未实现，需要时扩展 */
            //dataType: 0,// 新窗口填充的数据类型（0: url，1: html 数据，2: html 和 url 混合数据）
            //width: 0,// 要打开的窗口的宽；
            //height: 0,// 要打开的窗口的高
            //animDuration: 0,// 动画执行时间
            //extraInfo: {}// 扩展参数，设置值时，animDuration参数必传
        }, opts);
        var oParams = $.getUrlParams();
        var sWinName = opts.name,
            sWinKey,// 要打开的新窗口名
            args = opts.args,
            sUrl = opts.url,
            aParams = [];// 用于 url 中的参数拼接
        var oAllWin = $.getAllWin(),
            aWinName = oAllWin.n,
            aWinKey = oAllWin.k;
        // 根据 name 计算对应的 key，aWinName 允许重复，自动生成不重复的 aWinKey
        var fGenerateKey = function(sKey, i){
            var k = sKey + (i || '');
            return aWinKey.indexOf(k) === -1
                ? k : fGenerateKey(sKey, (i || 0) + 1);
        };
        var fBeforeOpen = function(){
            /* 处理 localStorage */
            aWinName.push(sWinName);// 记录打开的窗口 name
            aWinKey.push(sWinKey);// 记录打开的窗口 key
            appcan.locStorage.val('wiAppWin_win', JSON.stringify(oAllWin));
            // 处理 aParams
            (function(){
                aParams.push('win=' + sWinKey);
                // 源窗口为 opener.frame
                if(oParams['frame']){
                    aParams.push('opener=' + (oParams.opener || ROOTWIN));
                    aParams.push('openerf=' + oParams.frame);
                }
                // 源窗口为 win
                else{
                    aParams.push('opener=' + (oParams.win || ROOTWIN));
                }
                // 附加 args
                for(var k in args){
                    if(args.hasOwnProperty(k)){
                        aParams.push(k + '=' + encodeURIComponent(args[k]));
                    }
                }
            })();
            // 向 wiAppWin_blur 发送消息
            $.publishChanel({
                channelId: 'wiAppWin_blur',
                data: {
                    win: aWinName.length === 0
                        ? ROOTWIN : aWinKey[aWinName.length - 1]
                }
            });
        };

        sWinKey = fGenerateKey(sWinName);
        fBeforeOpen();
        appcan.window.open(sWinKey, sUrl + (sUrl.indexOf('?') === -1
                ? '?' : '&') + aParams.join('&'), opts.aniId, opts.type);
    };
    /**
     * @widoc $.openFrame
     * @namespace aux
     * @depends getUrlParams/$.getUrlParams
     * @des 初始化 frame 页面。
     * @param {object} opts
     * @param {string} opts.elId 参照元素 id（必须，因为 appcan.frame.open 只能由 id 指定参照元素），不支持 fReset 重设
     * @param {string=} opts.frameName frame 的标识，默认同 opts.elId。若当前页面中有多个 frame，要保证各不相同
     * @param {string} opts.url 必须，frame 对应页面路径
     * @param {number=} opts.ratio 屏幕密度，默认为 window.devicePixelRatio，一般不需自定义
     * 打开的新 frame 中，除了用户传入的 args，还包括：
     * - opener：执行打开新窗口操作的源窗口名
     * - frame：新 frame 的 name
     * 示例（此处 w1,w2,f1 不符合命名规则，仅为说明参数关系）：
     * - w1(opener:*, win:w1) => f2(opener:w1, frame:f2)
     * - 因为 frame 的操作依然要通过 win 为媒介，因此目前禁止从 frame 中打开其他 frame
     * @return {object|undefined} obj 接口对象
     * @rtn {function} obj.fReset(cusOpts) 修改 frame 配置
     */
    $.openFrame = function(opts){
        var oParams = $.getUrlParams();
        if(!opts.elId || !opts.url){
            console.error('openFrame error: elId 或 url 为空!');
            return;
        }
        if(oParams['frame']){
            console.error('暂不支持由 frame 创建新 frame!');
            return;
        }
        opts = $.extend({
            //elId:'content'
            //url:''
            frameName: '',
            ratio: window.devicePixelRatio || 1// 屏幕密度
        }, opts);
        var api;
        var sElId,// frame 位置参照元素 id
            sFrameName, nRatio,
            el;
        var fReset = function(cusOpts){
            var args, sUrl, oElPos;
            if(cusOpts){
                if(cusOpts.elId && cusOpts.elId != sElId){
                    console.error('openFrame error: 不允许重设参照元素');
                    return;
                }
                $.extend(opts, cusOpts);
            }
            args = opts.args;
            nRatio = opts.ratio;
            // frame 的值
            if(!opts.frameName){
                opts.frameName = sElId;
            }
            sFrameName = opts.frameName;
            // url 拼接，并附加 args
            sUrl = opts.url + '?opener=' + (oParams['win'] || ROOTWIN) + '&frame=' + sFrameName;
            for(var k in args){
                if(args.hasOwnProperty(k)){
                    sUrl += '&' + k + '=' + encodeURIComponent(args[k]);
                }
            }
            // 因为要执行刷新操作，因此 fReset 时也要重新打开 frame
            oElPos = el.offset();
            uexWindow.openPopover(sFrameName, 0, sUrl, '',
                oElPos.left * nRatio, oElPos.top * nRatio, el.width() * nRatio, el.height() * nRatio,
                parseInt($.styleHelper.getValue(el, 'font-size'), 10), 0, 0);
        };
        // resize时，手动调整 frame 的尺寸
        var fOnResize = function(){
            setTimeout(function(){// 需要延迟，待content大小变化完成后再对 frame 执行 resize
                var oElPos = el.offset();
                uexWindow.setPopoverFrame(sFrameName,
                    oElPos.left * nRatio, oElPos.top * nRatio, el.width() * nRatio, el.height() * nRatio);
            }, 0);
        };
        var fInit = function(){
            sElId = opts.elId;// 一旦初始化，不可修改
            el = $('#' + sElId);
            fReset();
            $.addResizeListener(fOnResize);
        };
        api = {
            fReset: fReset
        };
        fInit();
        return api;
    };
    /**
     * @widoc $.closeWin
     * @namespace aux
     * @depends getUrlParams/$.getUrlParams, chanel/$.publishChanel
     * @des 关闭窗口。
     * appcan.window.close()方法关闭非顶层窗口后，该窗口的画面仍然存在，会遮挡其之前打开的非关闭的窗口
     * @param {object} opts
     * @param {string=} opts.name 窗口 name，若未定义则关闭当前窗口
     * @param {string=} opts.key 窗口 win，定义时，opts.name 失效；opts.key 与 opts.name 均未定义时，关闭当前窗口
     * @param {number=} opts.aniId appcan.window.close(aniId,animDuration)中的aniId
     * @param {number=} opts.animDuration appcan.window.close(aniId,animDuration)中的animDuration
     */
    $.closeWin = function(opts){
        opts = $.extend({
            //name: '',
            //key: '',// 若定义了 key，name 将失效
            //aniId: int,
            //animDuration: int
        }, opts);
        var sName = opts.name,
            sKey = opts.key,
            sWin = $.getUrlParams().win,
            nIdx;
        var oAllWin = $.getAllWin(),
            aWinName = oAllWin.n,
            aWinKey = oAllWin.k,
            nLastIdx = aWinKey.length - 1;// 最后打开的窗口索引，关闭时用于判断是否为顶层的窗口
        /* 参数处理 */
        if(!sKey && sName){
            nIdx = aWinName.lastIndexOf(sName);
            sKey = aWinKey[nIdx];
        }
        else{
            if(!sKey){
                sKey = sWin;
            }
            nIdx = aWinKey.lastIndexOf(sKey);
            //sName = aWinName[nIdx];
        }
        /* 向 wiAppWin_focus 发送消息，由 $.onWinBlur() 监听 */
        if(nLastIdx === nIdx){// 要关闭的窗口是最后一个，会改变聚焦窗口
            $.publishChanel({
                channelId: 'wiAppWin_focus',
                data: {
                    win: nIdx === 0
                        ? ROOTWIN : aWinKey[nIdx - 1]
                }
            });
        }
        /* 修改本地缓存 */
        aWinName.splice(nIdx, 1);// 移除指定窗口 name
        aWinKey.splice(nIdx, 1);// 移除指定窗口 key
        appcan.locStorage.val('wiAppWin_win', JSON.stringify(oAllWin));
        /* 关闭操作，若要关闭的不是顶层窗口，关闭前应先隐藏，否则真机异常 */
        if(sKey === sWin){
            if(nIdx < nLastIdx){// 非顶层窗口，关闭前先隐藏
                uexWindow.setWindowHidden(1);
            }
            appcan.window.close(opts.aniId, opts.animDuration);
        }
        else{
            if(nIdx < nLastIdx){// 非顶层窗口，关闭前先隐藏
                appcan.window.evaluateScript(sKey, 'uexWindow.setWindowHidden(1)');
            }
            appcan.window.evaluateScript(sKey, 'appcan.window.close(' + opts.aniId + ',' + opts.animDuration + ')');
        }
    };
    /**
     * @widoc $.closeWinToTop
     * @namespace aux
     * @depends $.getAllWin, $.closeWin
     * @des 倒序关闭窗口，直到当前窗口（否则，当前窗口可能先被关闭，导致无法完成所有关闭操作）。
     * @param {object} opts
     * @param {number=} opts.aniId appcan.window.close(aniId,animDuration)中的aniId，仅作用于最后打开的窗口，其他为 0
     * @param {number=} opts.animDuration appcan.window.close(aniId,animDuration)中的animDuration 仅作用于最后打开的窗口，其他为 0
     */
    $.closeWinToTop = function(opts){
        opts = $.extend({
            //name: '',
            //aniId: int,
            //animDuration: int
        }, opts);
        var oAllWin = $.getAllWin(),
            aWinName = oAllWin.n,
            aWinKey = oAllWin.k,
            nLastIdx = aWinKey.length - 1;
        var sName = opts.name, sKey,
            sWin = $.getUrlParams().win,
            nIdx;
        if(typeof sName === 'string'){
            nIdx = aWinName.lastIndexOf(sName);
            sKey = aWinKey[nIdx];
            delete opts.name;
            appcan.window.evaluateScript(sKey, '$.closeWinToTop(' + JSON.stringify(opts) + ')');// 调用指定窗口的 $.closeWinToTop()，避免因为当前窗口先被关闭，导致操作中断
        }
        else{
            nIdx = aWinKey.lastIndexOf(sWin);
            //sKey = sWin;
            if(nLastIdx > nIdx){
                $.closeWin($.extend({// 最后一个窗口，支持动画等配置
                    key: aWinKey[nLastIdx]
                }, opts));
                for(var i = nLastIdx - 1; i > nIdx; i--){
                    $.closeWin({
                        key: aWinKey[i],
                        aniId: 0,
                        animDuration: 0
                    });
                }
            }
        }
    };
    /**
     * @widoc $.clearWinStorage
     * @namespace aux
     * @des 清空 localStorage 中 wiAppWin_win 的记录（在 app 首页打开时调用）。可以通过判断 url 中未定义 opener 等参数来确定 root 窗口，但存在风险（非调用接口打开的页面中引入 wiFrame 资源，会导致意外清空窗口记录）。
     * @param {object} opts
     */
    $.clearWinStorage = function(opts){
        appcan.locStorage.remove('wiAppWin_win');
    };
    /**
     * @widoc $.getAllWin
     * @namespace aux
     * @des 获取当前项目打开的所有窗口的 name 和 key。结构: {n: 打开的所有窗口 name, k: 打开的所有窗口 key}
     */
    $.getAllWin = function(){
        var sWin = appcan.locStorage.val('wiAppWin_win');
        return sWin ? JSON.parse(sWin) : {n: [], k: []};
    };
    /* 获取当前项目打开的所有窗口的 name，请用 $.getAllWin().n 替代。 */
    $.getAllWinName = function(){
        return $.getAllWin().n;
    };
})();